/*
Copyright 2010 Dmitry Barashev

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
package net.barashev.seclub.io.file;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.HashMap;
import java.util.Map;

import net.barashev.seclub.io.DataMarshaller;

public class MemoryFileManager implements FileManager {
    private static class MemoryFile<T> implements VFile<T> {
        private byte[] bytes;
        private final String name;
        private final DataMarshaller<T> marshaller;

        MemoryFile(String name, DataMarshaller<T> marshaller, byte[] bytes) {
            this.name = name;
            this.bytes = bytes;
            this.marshaller = marshaller;
        }
        public ReadableByteChannel getReadableChannel() {
            return Channels.newChannel(new ByteArrayInputStream(this.bytes));
        }
        public WritableByteChannel getWritableChannel() {
            final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            return new WritableByteChannel() {
                private final WritableByteChannel channel = Channels.newChannel(outputStream);
                public boolean isOpen() {
                    return this.channel.isOpen();
                }
                public void close() throws IOException {
                    this.channel.close();
                    MemoryFile.this.bytes = outputStream.toByteArray();
                }
                public int write(ByteBuffer src) throws IOException {
                    int result = this.channel.write(src);
                    MemoryFile.this.bytes = outputStream.toByteArray();
                    return result;
                }
            };
        }
        public FileChannel getFileChannel() {
            return new FileChannel() {
                long position = 0;

                @Override
                public void force(boolean metaData) throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public FileLock lock(long position, long size, boolean shared)
                throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public MappedByteBuffer map(MapMode mode, long position,
                        long size) throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public long position() throws IOException {
                    return this.position;
                }

                @Override
                public FileChannel position(long newPosition)
                throws IOException {
                    this.position = newPosition;
                    return this;
                }

                @Override
                public int read(ByteBuffer dst) throws IOException {
                    return read(dst, this.position);
                }

                @Override
                public int read(ByteBuffer dst, long position)
                throws IOException {
                    ByteArrayInputStream in = new ByteArrayInputStream(MemoryFile.this.bytes);
                    in.skip(position);
                    ReadableByteChannel channel = Channels.newChannel(in);
                    int result = channel.read(dst);
                    channel.close();
                    return result;
                }

                @Override
                public long read(ByteBuffer[] dsts, int offset, int length)
                throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public long size() throws IOException {
                    return MemoryFile.this.bytes.length;
                }

                @Override
                public long transferFrom(ReadableByteChannel src,
                        long position, long count) throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public long transferTo(long position, long count,
                        WritableByteChannel target) throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public FileChannel truncate(long size) throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public FileLock tryLock(long position, long size, boolean shared)
                throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                public int write(ByteBuffer src) throws IOException {
                    int result = write(src,this.position);
                    this.position += result;
                    return result;
                }

                @Override
                public int write(ByteBuffer src, long position)
                throws IOException {
                    ByteArrayOutputStream out = new ByteArrayOutputStream();
                    WritableByteChannel channel = Channels.newChannel(out);
                    int result = channel.write(src);
                    channel.close();
                    byte[] writtenBytes = out.toByteArray();
                    if (MemoryFile.this.bytes.length < position + writtenBytes.length) {
                        byte[] newBytes = new byte[(int) (position + writtenBytes.length)];
                        System.arraycopy(MemoryFile.this.bytes, 0, newBytes, 0, MemoryFile.this.bytes.length);
                        MemoryFile.this.bytes = newBytes;
                    }
                    System.arraycopy(writtenBytes, 0, MemoryFile.this.bytes, (int) position, writtenBytes.length);
                    return result;
                }

                @Override
                public long write(ByteBuffer[] srcs, int offset, int length)
                throws IOException {
                    throw new UnsupportedOperationException();
                }

                @Override
                protected void implCloseChannel() throws IOException {
                }

            };

		}
        public byte[] getContent() {
            return this.bytes;
        }
        public DataMarshaller<T> getMarshaller() {
            return this.marshaller;
        }
        public String getName() {
            return this.name;
        }
		@Override
		public long getLengthInBytes() {
			return bytes.length;
		}
    }

    private Map<String,MemoryFile> name2file = new HashMap<String,MemoryFile>();

    public <T> VFile<T> createFile(String name, DataMarshaller<T> marshaller) throws IOException {
        return createFile(name, marshaller, new byte[0]);
    }

    public <T> VFile<T> getFile(String name) {
        return this.name2file.get(name);
    }

    public <T> VFile<T> createFile(String name, DataMarshaller<T> marshaller, byte[] bytes) throws IOException {
        if (this.name2file.containsKey(name)) {
            throw new IllegalArgumentException("File "+name+" already exists");
        }
        MemoryFile<T> result = new MemoryFile<T>(name, marshaller, bytes);
        this.name2file.put(name, result);
        return result;
    }

    public Iterable<String> getFileNames() {
        return name2file.keySet();
    }
}
